/**
 * 
 */
(function() {
	// global defs
	Template = {
		compile : _Template,
		options : {
			DEBUG : false
		},
		addTagHandlers : function(map) {
			tagHandlers = merge(map, tagHandlers);
		},
		addFilters : function(map) {
			filters = merge(map, filters);
		}
	};

	// local defs
	var _REGEX_ = /{[{%#]([^{]*)[}%#]}/;
	var parserCache = {};
	var filters = {
		upper : function() {
			return function(v) {
				return ('' + v).toUpperCase();
			};
		},
		truncatechars : function(o) {
			var len = 10;
			try {
				len = parseInt(o[0]);
			} catch (e) {
				if (Template.options.DEBUG) {
					throw e;
				}
			}
			return function(v) {
				if (v.length < len)
					return v;
				else
					return v.substring(0, len - 3) + '...';
			};
		}
	};
	var tagHandlers = {
		'now' : {
			blocks : 0,
			builder : function(contents) {
				// TODO: formatter
				return function(ctx) {
					return new Date();
				};
			}
		},
		'spaceless' : {
			blocks : 1,
			builder : function(contents, tokens) {
				return function(ctx) {
					return map(tokens, function(x) {
						return x(ctx);
					}).join('');
				};
			}
		},
		'for' : {
			blocks : 2,
			split : 'empty',
			builder : function(contents, tokens1, tokens2) {
				var body = [
						'with (ctx){var count=0;',
						'var str= "";for (var ',
						contents.slice(1).join(' '),
						'){',
						'count++;ctx["',
						contents[1],
						'"] = ',
						contents[3],
						'[',
						contents[1],
						'];',
						'str += map(tokens1, function(t){return t(ctx);}).join("");',
						'} if (count==0) {',
						'str = map(tokens2, function(t){return t(ctx);}).join("");',
						'}', 'return str;}' ].join('');
				try {
					var loopFn = new Function('ctx', 'tokens1', 'token2',
							'map', body);
				} catch (e) {
					throw 'Error while parsing: "' + contents.join(' ') + '".';
				}
				return function(ctx) {
					try {
						return loopFn(ctx, tokens1, tokens2, map);
					} catch (e) {
						if (Template.options.DEBUG) {
							throw e;
						} else {
							return '';
						}
					}
				};
			}
		},
		'if' : {
			blocks : 2,
			split : 'else',
			builder : function(contents, tokens1, tokens2) {
				var expr = contents.slice(1).join(' '), tokens = null;
				return function(ctx) {
					try {
						with (ctx) {
							tokens = eval(expr) ? tokens1 : tokens2;
						}
						return map(tokens, function(x) {
							return x(ctx);
						}).join('');
					} catch (e) {
						if (Template.options.DEBUG) {
							throw e;
						} else {
							return '';
						}
					}
				};
			}
		},
		'regroup' : {
			blocks : 1,
			builder : function(contents, tokens) {

			}
		}
	};

	function _Template(txt, /* optional */data) {
		options = Template.options;
		
		function trim(s) {
			return s.replace(/^\s+|\s+$/g, '');
		}
		
		function parseTag(tokens) {
			var m = null, token = tokens[0];
			if ((m = token.match(/{#(.*)#}/))) {
				return null;
			} else if ((m = token.match(/{{(.*)}}/))) {
				return varParser(m[1], tokens);
			} else if (m = token.match(/{%(.*)%}/)) {
				var parts = trim(m[1]).split(/\s+/);
				return getParser(parts[0])(parts, tokens);
			}
			// text
			return {
				handler : function(ctx) {
					return token;
				},
				rest : tokens.slice(1)
			};
		}

		function parseUntil(ta, /* array of tag names */tagNames) {
			var handlers = [], matched = null;
			while (ta.length > 0 && !matched) {
				var token = ta[0], m = null, name = (m = token
						.match(/{%(.*)%}/)) ? trim(m[1]) : null;
				if (contains(tagNames, name)) {
					matched = name;
					ta = ta.slice(1);
				} else {
					var parseResult = parseTag(ta);
					handlers.push(parseResult.handler);
					ta = parseResult.rest;
				}
			}
			return {
				handlers : handlers,
				matched : matched,
				rest : ta
			};
		}

		function getParser(tagName) {
			if (!parserCache[tagName]) {
				var conf = tagHandlers[tagName], p = null;
				if (!conf)
					throw 'No handler for tag "' + tagName + '".';
				switch (conf.blocks) {
				case 0:
					p = function(tagContents, tokens) {
						return {
							handler : conf.builder(tagContents),
							rest : tokens.slice(1)
						};
					};
					break;
				case 1:
					var endTag = 'end' + tagName;
					p = function(tagContents, tokens) {
						var result = parseUntil(tokens.slice(1), [ endTag ]);
						return {
							handler : conf
									.builder(tagContents, result.handlers),
							rest : result.rest
						};
					};
					break;
				default:
					var endTag = 'end' + tagName;
					p = function(tagContents, tokens) {
						var children1 = [], children2 = [];
						var result = parseUntil(tokens.slice(1), [ endTag,
								conf.split ]);
						if (!result.matched || result.matched == endTag) {
							children1 = result.handlers;
						} else {
							children1 = result.handlers;
							tokens = result.rest;
							result = parseUntil(tokens, [ endTag ]);
							children2 = result.handlers;
						}
						return {
							handler : conf.builder(tagContents, children1,
									children2),
							rest : result.rest
						};
					};
					break;
				}
				parserCache[tagName] = p;
			}
			return parserCache[tagName];
		}

		if (txt.match(/^#/)) {
			var el = document.getElementById(txt.substring(1));
			if (el)
				txt = el.innerHTML;
			else if (options.DEBUG)
				throw 'Element "' + txt + '" could not be found.';
			else
				return '';
		}
		var tokens = partition(txt, _REGEX_), handlers = [];
		while (tokens.length > 0) {
			var parseResult = parseTag(tokens);
			if (!parseResult) {
				tokens = tokens.slice(1);
			} else {
				handlers.push(parseResult.handler);
				tokens = parseResult.rest;
			}
		}
		function render(data) {
			return map(handlers, function(t) {
				return t(data);
			}).join('');
		}
		;
		if (data)
			return render(data);
		else
			return render;
	}

	function varParser(tagContents, tokens) {
		// TODO: formatters
		var parts = tagContents.split('|');
		tagContents = parts[0];
		var filterFn = reduce(map(parts.slice(1), function(s) {
			var p = s.split(':');
			s = p[0];
			return (s && filters[s] && filters[s](p.slice(1))) || function(v) {
				return v;
			};
		}), function(f1, f2) {
			return function(v) {
				return f1(f2(v));
			};
		}, function(v) {
			return v;
		});
		return {
			handler : function(ctx) {
				try {
					with (ctx) {
						return filterFn(eval(tagContents));
					}
				} catch (e) {
					if (Template.options.DEBUG) {
						throw e;
					} else {
						return '';
					}
				}
			},
			rest : tokens.slice(1)
		};
	}

	function contains(a, obj) {
		for ( var i = 0; i < a.length; i++) {
			if (a[i] === obj) {
				return true;
			}
		}
		return false;
	}

	function merge(obj1, obj2) {
		function copy(oDest, oSource) {
			for ( var p in oSource) {
				if (p && oSource.hasOwnProperty(p)) {
					oDest[p] = oSource[p];
				}
			}
		}
		var o = {};
		copy(o, obj1);
		copy(o, obj2);
		return o;
	}

	function map(arr, fn) {
		if (!arr || ((arr instanceof Array) && arr.length == 0))
			return [];
		var result = [];
		for ( var p in arr) {
			if (p && arr.hasOwnProperty(p)) {
				result[p] = fn(arr[p]);
			}
		}
		return result;
	}

	function reduce(arr, fn, /* optional */startVal) {
		if (!arr || ((arr instanceof Array) && arr.length == 0))
			return startVal || null;
		var index = 0;
		function getNext() {
			return index < arr.length? arr[index++] : null;
		}
		startVal = startVal || getNext();
		var next = getNext();
		for(;next;next = getNext())
			startVal = fn(startVal, next);
		return startVal;
	}

	// TODO: add position info
	function partition(str, regex) {
		var tokens = [];
		var index = 0;
		var tokenStr = null;
		var results = null;
		while (str.length > 0) {
			results = regex.exec(str);
			if (!results) {
				tokens.push(str);
				break;
			}
			index = results.index;
			if (index > 0) {
				tokens.push(str.substring(0, index));
				str = str.substring(index);
			}
			tokenStr = results[0];
			tokens.push(tokenStr);
			str = str.substring(tokenStr.length);
		}
		return tokens;
	}
})();